[리눅스 커널 2.6의 세계] ③ 네트워크 서브 시스템 :: 리눅스일반[SSISO Community]

덧글  쓰기  |  엮인글  쓰기  
이  포스트를..  



                        더보기"  name=source_sumtext>      만두양  관심거리  ^-^  [본문스크랩]  [리눅스  커널  2.6의  세계]  ③  네트워크  서브  시스템    
                                만두양  관심거리  ^-^  [본문스크랩]  [리눅스  커널  2.6의  세계]  ③  네트워크  서브  시스템        
 
SSISO 카페 SSISO Source SSISO 구직 SSISO 쇼핑몰 SSISO 맛집
추천검색어 : JUnit   Log4j   ajax   spring   struts   struts-config.xml   Synchronized   책정보   Ajax 마스터하기   우측부분




<그림  5>  전송  계층




<그림  4>  네트워크  계층




<그림  3>  데이터  링크  계층




<그림  2>  3-way  handshake  in  TCP
리눅스일반


네트워크  서브  시스템은  리눅스가  지금처럼  널리  확산되는데  많은  공헌을  했으며,  리눅스의  최대  장점  중의  하나로  인식되고  있는  분야이다.  이처럼  중요한  위치를  차지하고  있음에도  지금껏  리눅스  커널의  네트워크  서브  시스템의  구조를  분석하고  이해하려는  시도가  많이  부족한  것이  사실이다.

이번  글에서는  리눅스의  최대  장점  중  하나로  꼽히는  네트워킹  부분에  대한  구현을  살펴보겠다.  네트워크  코드는  너무나  방대한  영역이기  때문에  한  번에  살펴보는  것이  불가능하므로  아주  단순한  소켓  프로그램을  예제로  하여  기본적인  소켓의  생성,  연결,  데이터  전송/수신  과정에  대해  살펴보기로  한다.  네트워크는  또한  보안에  민감한  영역이기  때문에  곳곳에  보안을  위한  코드들이  포함되어  있음을  확인할  수  있을  것이다(가장  최신  버전의  안정  커널인  2.6.10에  대해  살펴본다).

자료  구조
소켓  버퍼  -  sk_buff  구조체
소켓  버퍼는  네트워크로  전송되는  패킷을  나타내는  자료  구조로서,  네트워크  서브  시스템  전반에서  사용되는  중요한  구조체이다.  소켓  버퍼를  정의한  sk_buff  구조체는  <include/linux/skbuff.h>에  정의되어  있다.

next,  prev,  list는  소켓  버퍼를  관리하기  위한  포인터이다.  소켓  버퍼를  저장하는  큐는  sk_buff_head  구조체의  형태로  각  소켓  버퍼를  이중  연결  리스트로  관리한다.  sk는  소켓  버퍼가  속한  소켓을  나타내며,  stamp는  패킷을  받은  시간을  저장한다.  net_device  구조체의  dev,  input_dev,  real_dev  필드는  현재  패킷을  받거나  보내기  위한  네트워크  장치를  가리키는  변수이다.  다음으로  나오는  3개의  union  필드들은  각각  OSI  7  계층의  전송  계층(transport  layer),  네트워크  계층(network  layer),  데이터  링크  계층(data  link  layer)의  헤더  정보를  저장한다.  

이들  헤더  정보들은  데이터  영역  내에  순서대로  저장되어  있으며,  각각의  계층을  지나면서  해당  계층의  프로토콜에  맞는  헤더  정보를  적절히  설정한다.  dst  필드는  패킷을  전송하기  위한  정보를  저장하는  구조체이다.  cb는  각  프로토콜에서  사용되는  제어  정보들을  저장하는  역할을  하는  버퍼이다(control  buffer).  truesize  필드는  sk_buff  구조체  자체의  크기에  데이터  영역의  크기를  더한  실제  소켓  버퍼  구조체의  크기를  나타낸다.

소켓  버퍼  내의  데이터에  접근하기  위한  필드로  head,  data,  tail,  end가  있다.  이  중  head와  end는  처음에  할당한  데이터  영역의  시작과  끝을  가리키는  고정된  필드이다.  data와  tail은  그  중에서  실제로  데이터가  저장된  영역의  시작과  끝을  가리키는  필드로  소켓  버퍼로  데이터가  추가될  때마다  변경된다.  소켓  버퍼의  내용은  실제  데이터  앞에  각  계층  별로  헤더  정보가  추가되는  형태이므로  데이터  영역의  처음부터  사용할  수  없기  때문에  이러한  필드를  이용하여  쉽게  접근할  수  있도록  한다.

그리고  실제  구조체에는  포함되어  있지  않지만  소켓  버퍼의  데이터를  관리하기  위해  데이터  영역의  뒷부분에  추가적으로  struct  skb_shared_info  구조체가  사용된다.  이  구조체는  데이터  영역을  참조하고  있는  소켓  버퍼의  수,  fragment를  이루는  소켓  버퍼  정보  등을  포함한다.  소켓  버퍼의  대략적인  형태는  <그림  1>과  같이  나타낼  수  있다.








<그림  1>  소켓  버퍼(struct  sk_buff)
[1]
등록일:2006-05-22 22:57:34 (0%)
작성자:
제목:[리눅스 커널 2.6의 세계] ③ 네트워크 서브 시스템


소켓  버퍼를  다루기  위한  여러  함수들이  존재한다.  먼저  소켓  버퍼를  할당하기  위해  alloc_skb  함수가  사용된다.  이  함수는  주어진  크기만큼의  데이터  영역을  가지는  소켓  버퍼를  생성한다.  또한  디바이스  드라이버에서  패킷을  수신했을  때  소켓  버퍼를  생성하기  위해  사용하는  dev_alloc_skb  함수가  있다.  이  함수는  헤더  정보를  포함하기  위해  주어진  크기보다  16바이트  만큼을  더하여  소켓  버퍼를  생성하고,  skb_reserve  함수로  16바이트  만큼을  예약해  둔다.  생성된  소켓  버퍼는  kfree_skb  함수를  통해  해제된다.

또한  소켓  버퍼를  복사하기  위한  skb_copy(데이터  영역도  복사),  skb_clone(데이터  영역  공유)  함수와  데이터  영역을  가리키는  포인터를  조작하기  위한  skb_put,  skb_push,  skb_pull  등의  함수가  있다.  그리고  소켓  버퍼를  큐에  넣거나  빼는  일을  수행하는  skb_queue_tail,  skb_dequeue,  skb_insert,  skb_append,  skb_unlink  등의  함수도  제공한다.

네트워크  장치  -  net_device  구조체
net_device  구조체는  리눅스  커널  내에서  네트워크  장치를  표현하기  위해  사용하는  구조체이다.  네트워크  장치는  일반  블럭  장치나  문자  장치와는  달리  /dev  디렉토리  내에  특정한  장치  파일을  가지지  않으며,  단순한  read/write  연산만으로는  접근할  수  없으므로  일반  장치와는  달리  취급된다.  net_device  구조체는  I/O  연산에  필요한  하드웨어  정보  뿐  아니라  이를  관리하기  위한  고수준의  자료  구조  및  함수에  대한  정보를  포함하는  거대한  구조체로  네트워크  서브  시스템  전반에  걸쳐  사용된다.  net_device  구조체는  <include/linux/netdevice.h>에  정의되어  있다.

먼저  net_device  구조체의  앞쪽에  나오는  하드웨어  정보를  살펴보기로  한다.  name은  네트워크  장치가  가질  이름을  저장한다.  이더넷  장치의  이름은  특별히  지정하지  않는  한  <net/ethernet/eth.c>에  정의된  alloc_etherdev()  함수에  의해  eth0부터  차례로  부여된다.  다음으로  장치가  사용할  공유  메모리  영역,  I/O  기본  주소,  인터럽트  번호  등의  정보를  저장한  후  특정  하드웨어  요구하는  포트  번호와  DMA  채널  번호를  각각  저장한다.

state  필드는  장치의  상태를  나타내는  것으로  장치가  open되어  동작할  준비가  된  경우에는  __LINK_STATE_START  값이  설정되고,  장치의  버퍼가  가득차서  더  이상  패킷을  처리할  수  없는  경우  __LINK_STATE_XOFF  값으로  설정된다.  <include/linux/net_device.h>에  정의된  netif_running()와  netif_queue_stopped()  함수는  각각  이  state  필드를  검사하여  적절한  값을  리턴한다.  가능한  모든  상태의  목록은  역시  <include/linux/net_device.h>에  enum  netdev_state_t로  정의되어  있다.

ifindex  필드는  장치의  이름과  마찬가지로  해당  장치를  나타내는  역할을  하는  정수  값으로  dev_new_index()  함수에  의해  부여된다.  이후  dev_get_by_index(),  dev_get_by_name()  등의  함수로  장치의  레퍼런스를  얻어오는  것이  가능하다.  iflink  필드는  패킷을  전송할  네트워크  장치의  인덱스를  저장하는  변수로  기본적으로  ifindex  필드와  같은  값을  가지지만  터널링  장치와  같은  경우에는  실제로  패킷을  전송할  다른  장치의  인덱스  값을  가지게  된다.  get_stats  필드는  장치의  통계  정보(struct  net_device_stats)를  얻기  위한  함수의  포인터를  저장한다.

mtu  필드는  장치가  전송할  수  있는  최대  패킷  크기(MTU  :  Maximum  Transfer  Unit)  정보를  저장하며,  type  필드는  하드웨어의  종류(Ethernet,  APPLEtalk,  ATM,  IrDA  등)에  대한  정보를  저장한다.  hard_header_len  필드는  데이터  링크  계층에서  필요한  헤더  정보의  길이를  나타내며,  priv  필드는  하드웨어의  종류에  따라  특정한  정보를  저장할  목적으로  사용된다.  <net/ethernet/eth.c>에  정의된  ether_setup()  함수에서  이더넷  하드웨어  장치에  대한  설정을  실행한다.  다음으로는  장치의  하드웨어  주소와  브로드  캐스트  용  주소,  하드웨어  주소의  길이  등을  저장한다.


...

/*  Protocol  specific  pointers  */

void    *atalk_ptr;    /*  AppleTalk  link  */
void    *ip_ptr;    /*  IPv4  specific  data  */  
void    *dn_ptr;    /*  DECnet  specific  data  */
void    *ip6_ptr;    /*  IPv6  specific  data  */
void    *ec_ptr;    /*  Econet  specific  data  */
void    *ax25_ptr;    /*  AX.25  specific  data  */

struct  list_head    poll_list;    /*  Link  to  poll  list    */
int    quota;
int    weight;

struct  Qdisc    *qdisc;
struct  Qdisc    *qdisc_sleeping;
struct  Qdisc    *qdisc_ingress;
struct  list_head    qdisc_list;
unsigned  long    tx_queue_len;  /*  Max  frames  per  queue  allowed  */

...

/*  Pointers  to  interface  service  routines.    */
int    (*open)(struct  net_device  *dev);
int    (*stop)(struct  net_device  *dev);
int    (*hard_start_xmit)  (struct  sk_buff  *skb,
                  struct  net_device  *dev);
...
}

다음은  디바이스  드라이버  영역에서  사용될  고수준의  정보들이다.  먼저  상위의  프로토콜에  따른  정보들을  저장하기  위한  포인터  변수들을  각각  유지한다.

qdisc  필드는  장치에서  패킷  정보를  저장할  큐에  대한  정보를  나타낸다.  패킷을  전송하는  경우  장치가  큐를  지원한다면  장치의  qdisc가  가리키는  큐에  소켓  버퍼  데이터를  저장해  두었다가  나중에  처리하고  그렇지  않다면(루프백  장치나  IP  터널링  같은  소프트웨어적인  장치의  경우)  바로  전송한다.  tx_queue_len  필드는  큐에  저장될  수  있는  최대  소켓  버퍼의  수를  나타낸다.  그리고  상위  계층에서  장치에  대한  연산을  수행하기  위해  호출되는  함수들의  포인터를  저장한다.

간단한  소켓  프로그래밍  예제
다음은  단순한  에코  클라이언트  프로그램으로  W.  Richard  Stevens의  『Unix  Network  Programming』이라는  책의  1장에  나오는  예제를  약간  수정한  것이다.  간략한  설명을  위해  대부분의  에러  처리  부분은  생략했고  write  부분을  추가했다.  이  프로그램을  실행시킨다면  서버에  "hello"라는  문자열을  전송한  뒤  똑같이  "hello"라는  문자열을  서버로부터  받게  될  것이다.  다음의  예제에서  주의  깊게  봐야  할  함수는  socket,  connect,  write,  read의  네  가지이다.  이들  각각에  대해  커널  내부에서  어떤  일이  일어나는지  살펴보자.


#include  "unp.h"

int  main(int  argc,  char  **argv)
{
  int  sockfd;
  char  line[MAXLINE  +  1];
  struct  sockaddr_in  servaddr;

  if  (argc  !=  2)
    err_quit("usage:  a.out  <IPaddress>");

  sockfd  =  socket(AF_INET,  SOCK_STREAM,  0);

  bzero(&servaddr,  sizeof(servaddr));
  servaddr.sin_family  =  AF_INET;
  servaddr.sin_port  =  ntons(7);
  inet_pton(AF_INET,  argv[1],  &servaddr,sin_addr);

  connect(sockfd,  (SA  *)  &servadr,  sizeof(servaddr));

  write(sockfd,  "hello",  5);
  read(sockfd,  recvline,  MAXLINE);
  recvline[n]  =  0;  /*  null  terminate  */
  fputs(recvline,  stdout);

  exit(0);
}

소켓의  생성과  연결
socket()  시스템  콜
먼저  소켓의  생성을  위해  socket  시스템  콜을  호출하면  리눅스  시스템  콜의  처리  방식에  따라  대응하는  커널  처리  루틴인  sys_socket()  함수가  호출된다.  이  함수는  <net/socket.c>에  정의되어  있으며  sock_create()  함수를  이용하여  소켓  구조체를  생성하고  이를  sock_map_fd()  함수를  이용해  파일  디스크립터에  연결한  뒤  이  값을  리턴한다.  sock_create()  함수는  바로  __sock_create()  함수를  호출하며  이  함수가  실제  소켓을  생성하는  일을  수행한다.


static  int  __sock_create(int  family,  int  type,  int  protocol,  struct  socket  **res,  int  kern)
{
  int  i;
  int  err;
  struct  socket  *sock;

  if  (family  <  0  ||  family  >=  NPROTO)
    return  -EAFNOSUPPORT;
  if  (type  <  0  ||  type  >=  SOCK_MAX)
    return  -EINVAL;

  if  (family  ==  PF_INET  &&  type  ==  SOCK_PACKET)  {
    static  int  warned;  
    if  (!warned)  {
        warned  =  1;
        printk(KERN_INFO  "%s  uses  obsolete  (PF_INET,SOCK_PACKET)\n",  current->comm);
    }
    family  =  PF_PACKET;
  }

  err  =  security_socket_create(family,  type,  protocol,  kern);
  if  (err)
    return  err;

#if  defined(CONFIG_KMOD)
  if  (net_families[family]==NULL)
  {
    request_module("net-pf-%d",family);
  }
#endif

  net_family_read_lock();
  if  (net_families[family]  ==  NULL)  {
    i  =  -EAFNOSUPPORT;
    goto  out;
  }

먼저  인자로  주어진  family와  type  변수가  올바른  값인지를  검사한다.  앞의  예제의  경우라면  PF_INET과  SOCK_STREAM이  넘어오게  된다.  PF_INET의  PF는  ‘Protocol  Family’를  의미하며  AF(Address  Family)에  해당하는  값과  동일하다.  리눅스에서  지원하는  프토토콜의  목록은  <include/linux/socket.h>에  정의되어  있다.  그리고  호환성을  위해  PF_INET에  대하여  SOCK_PACKET  타입을  명시한  경우  family  값을  PF_PACKET으로  수정한다.  그리고  security_socket_create()  함수를  먼저  호출하여  소켓을  생성하기  위한  보안  사항을  점검한다.  

이를  위해  security_operations  구조체의  socket_create  필드가  가리키는  함수가  호출된다.  별도의  security_operations  구조체가  등록되지  않았다면  이  함수의  기본  값은  dummy_socket_create()  함수로  단순히  0을  리턴한다.  이후에  net_families  변수가  저장하고  있는  등록된  프로토콜의  배열에서  주어진  family가  존재하는지  검사한다.  만약  커널  모듈을  지원하는  경우라면(존재하지  않는  경우)  request_module()  함수를  이용하여  모듈을  요청하고  net_family  구조체를  읽기  위한  락을  획득한다.


if  (!(sock  =  sock_alloc()))  
   {
      printk(KERN_WARNING  "socket:  no  more  sockets\n");
      i  =  -ENFILE;   /*  Not  exactly  a  match,  but  its  the
         closest  posix  thing  */
      goto  out;
   }

     sock->type  =  type;

   i  =  -EAFNOSUPPORT;
   if  (!try_module_get(net_families[family]->owner))
      goto  out_release;

   if  ((i  =  net_families[family]->create(sock,  protocol))  <  0)
      goto  out_module_put;

   if  (!try_module_get(sock->ops->owner))  {
      sock->ops  =  NULL;
      goto  out_module_put;
   }

   module_put(net_families[family]->owner);
   *res  =  sock;
   security_socket_post_create(sock,  family,  type,  protocol,  kern);

out:
   net_family_read_unlock();
   return  i;
out_module_put:
   module_put(net_families[family]->owner);
out_release:
   sock_release(sock);
   goto  out;
}

그리고는  sock_alloc()  함수를  이용하여  BSD  소켓  구조체(struct  socket)를  생성한다.  생성된  소켓의  타입에  인자로  주어진  type  변수를  설정하고  try_module_get()  함수를  이용하여  family  인자가  가리키는  프로토콜이  모듈로  구현된  경우  사용  카운터를  증가시킨다.  다음으로  주어진  프로토콜(family)에  맞는  net_families  구조체의  멤버인  create()  함수를  호출하여  커널에서  사용할  소켓  구조체(struct  sock)를  생성한다.  

앞의  경우  inet_family_ops  구조체의  create()  함수인  inet_create()  함수가  호출된다.  커널에서는  이렇게  생성된  INET  소켓(struct  sock)을  사용하여  필요한  작업을  처리하지만  사용자  레벨에서는  BSD  소켓(struct  socket)  인터페이스를  사용하여  프로그래밍이  이루어진다.  그리고는  try_module_get()  함수를  이용하여  소켓  관련  연산자  구조체가  존재하는지  검사한  뒤  res  변수에  생성된  소켓의  포인터를  저장한다.  마지막으로  security_socket_post_create()  함수를  호출하여  보안  사항을  점검한  뒤  net_family  구조체에  대한  락을  해제하고  리턴한다.

connect()  시스템  콜
connect()  부분은  소켓을  통해  통신할  상대방  측과의  신뢰성  있는  연결을  확립하는  과정이다.  먼저  conect()를  호출한  측(클라이언트)에서  연결을  요청하기  위해  SYN이라는  형태의  패킷을  상대방(서버)에게  보낸다.  SYN  패킷을  받은  서버는  이에  대한  확인을  위해  SYN-ACK  패킷을  보내고,  마지막으로  클라이언트가  이에  대한  응답으로  ACK  패킷을  서버에게  보냄으로써  연결이  성립되는  형태이다.  이렇게  연결  요청시  총  3단계로  패킷을  주고받기  때문에  3-way  handshake라고  한다.





connect()  시스템  콜도  마찬가지로  sys_connect()  함수에서  처리된다.  먼저  인자로  주어진  파일  디스크립터를  통해  해당  BSD  소켓의  정보를  얻어온  후  소켓에  연관된  연산자  구조체의  connect()  함수를  호출한다.  socket()  시스템  콜을  호출할  때  PF_INET,  SOCK_STREAM으로  설정했으므로  이  과정에서  <net/ipv4/af_inet.c>에  정의된  inet_stream_ops  구조체의  inet_stream_connect()  함수가  호출된다.  이  함수는  BSD  소켓  구조체의  state  필드를  검사하여  아직  connect가  호출되지  않은  SS_UNCONNECTED  상태라면  해당  소켓에  연관된  프로토콜의  connect()  함수를  다시  호출하고  타임아웃에  관련된  처리를  한  후  state를  SS_CONNECTED  상태로  변경한다.

TCP  프로토콜에서  처리하는  connect  함수는  tcp_prot  구조체의  tcp_v4_connect()이다.  이  함수는  <net/ipv4/tcp_ipv4.c>에  정의되어  있으며,  먼저  connect()  시스템  콜의  인자로  주어진  소켓  주소에  대해  ip_route_connect()  함수를  호출하여  라우팅  테이블을  검색하고  패킷을  전송할  목적지  정보를  얻어온다.  이렇게  얻어온  정보를  이용하여  INET  소켓의  정보를  적절히  설정하고  sk_state  필드를  TCP_SYN_SENT  상태로  변경한다.  

그리고는  tcp_v4_hash_connect()  함수를  호출하여  클라이언트  측의  포트를  자동으로  할당한다.  이  값은  sysctl_local_port_range[0],  sysctl_local_port_range[1](기본  값은  1024,  4999)  사이의  값으로  할당  가능하며,  이전에  할당된  값이  tcp_port_rover  변수에  저장되어  있으므로(초기  값은  1023)  이  값보다  1만큼  더  큰  값에서부터  검색을  시작한다.  이렇게  포트가  할당되면  ip_route_newports()  함수를  다시  호출하여  새로  할당된  포트에  대해  라우팅  테이블의  변경  사항이  있는지  다시  검색한다.  그리고  마지막으로  tcp_connect()  함수를  호출하여  SYN  패킷을  위한  소켓  버퍼를  생성하고  tcp_transmit_skb()  함수를  통해  전송한다.

그리고  이후에는  서버  측에서  SYN-ACK  패킷이  도착하기를  기다리게  된다.  SYN-ACK  패킷을  수신했다면  tcp_rcv_state_process()에  의해  tcp_rcv_synsent_state_process()  함수가  호출된다.  한  번에  전송할  수  있는  패킷의  최대  크기인  MSS(Maximum  Segment  Size)  값을  동기화하고  sk_state  필드를  TCP_ESTABLISHED  상태로  변경한  후  ACK  패킷을  서버로  전송한다.

패킷의  전송
응용  계층  -  Echo  client
응용  프로그램에서  네트워크로  데이터를  전송하기  위해서는  생성된  소켓에  write,  send,  sendto,  sendmsg  등의  시스템  콜을  사용할  수  있다.  여기서는  가장  일반적인  형태인  write  연산에  대해  살펴보도록  하겠다.  write  시스템  콜을  호출하면  커널의  sys_write()  함수가  호출된다.  이  함수는  리눅스의  VFS(Virtual  File  System)  형식을  따라  주어진  파일에  맞는  연산을  처리할  수  있도록  vfs_write()  함수를  호출하며  결국  file  구조체의  f_op  연산자  구조체에서  write  필드가  가리키는  함수를  호출한다.  다음은  <net/socket.c>에  정의된  소켓에  대한  f_op  연산자  구조체이다.


static  struct  file_operations  socket_file_ops  =  {
  .owner  =    THIS_MODULE,
  .llseek  =    no_llseek,
  .aio_read  =    sock_aio_read,
  .aio_write  =    sock_aio_write,
  .poll  =    sock_poll,
  .ioctl  =      sock_ioctl,
  .mmap  =    sock_mmap,
  .open  =    sock_no_open,  /*  special  open  code  to  disallow  open  via  /proc  */
  .release  =    sock_close,
  .fasync  =    sock_fasync,
  .readv  =    sock_readv,
  .writev  =    sock_writev,
  .sendpage  =    sock_sendpage
};

여기서  볼  수  있듯이  socket_file_ops  구조체에서는  write  연산을  정의하지  않았다.  이  경우  vfs_write()  함수는  do_sync_write()  함수에  의해  aio_write()  함수를  호출하도록  되어  있으므로  결국  sock_aio_write()  함수가  호출된다.  sock_aio_write()  함수는  적절한  인자를  설정한  후  __sock_sendmsg()  함수를  호출하게  된다.  앞에서는  write  시스템  콜에  관해  살펴봤지만  writev,  send,  sendto,  sendmsg  시스템  콜을  호출한  경우에도  결과적으로는  sock_sendmsg()  함수가  호출되고,  이  함수는  다시  __sock_sendmsg()  함수를  호출하기  때문에  이후의  과정은  모두  동일하게  처리된다.


static  inline  int  __sock_sendmsg(struct  kiocb  *iocb,  struct  socket  *sock,  
        struct  msghdr  *msg,  size_t  size)
{
    struct  sock_iocb  *si  =  kiocb_to_siocb(iocb);
    int  err;

    si->sock  =  sock;
    si->scm  =  NULL;
    si->msg  =  msg;
    si->size  =  size;

    err  =  security_socket_sendmsg(sock,  msg,  size);
    if  (err)
        return  err;

    return  sock->ops->sendmsg(iocb,  sock,  msg,  size);
}

__sock_sendmsg()  함수가  호출된  시점에서  msg  인자의  msg_iov  필드가  가리키는  iovec  구조체의  iov_base는  write()  시스템  콜이  호출될  때  주어진  사용자  공간의  데이터인  "hello"를  가리키며  size  인자는  5가  된다.  이  함수는  소켓  I/O  연산에  필요한  sock_iocb  구조체를  적절히  설정한  뒤  security_socket_sendmsg()  함수를  호출하여  보안  사항을  점검한다.  이후에  BSD  소켓  구조체의  ops  연산자  구조체에  있는  sendmsg  필드에  저장된  함수를  호출한다.

이와  같이  PF_INET으로  소켓을  생성한  경우  inet_stream_ops  구조체의  inet_sendmsg()  함수가  호출된다.  inet_sendmsg()  함수는  주어진  소켓에  대한  INET  소켓의  정보를  얻어온  후  sk_prot  구조체의  sendmsg  필드가  가리키는  함수를  주어진  인자와  함께  호출한다(여기서  BSD  소켓이  INET  소켓으로  바뀌어  넘겨진다).  우리는  SOCK_STREAM  인자를  주어  소켓을  생성했기  때문에  이  과정에서  최종적으로  TCP  프로토콜의  sendmsg  처리  함수인  tcp_sendmsg()  함수가  호출된다.

전송  계층  -  TCP
tcp_sendmsg()  함수는  <net/ipv4/tcp.c>에  정의되어  있다.  먼저  소켓에  대한  락을  획득하고  msg에  대한  플래그가  있다면  설정한다.  TCP_CHECK_TIMER()  매크로는  현재  아무런  작업도  수행하지  않는다.  sock_sndtimeo()  함수는  패킷  전송시  대기할  시간을  MSG_DONTWAIT  플래그가  설정된  경우  0으로  그렇지  않다면  sk_sndtimeo  값으로  설정한다.

sk_sndtimeo  값은  setsockopt()  시스템  콜을  통해  특별히  지정하지  않았으므로  MAX_SCHEDULE_TIMEOUT  (=  LONG_MAX)  값을  가지며  실제적으로  거의  무한정  기다리게  된다.  그리고  현재  소켓의  상태가  연결이  확립된  상태(TCPF_ESTABLISHED)가  아니라면  sk_stream_wait_connect()  함수를  통해  timeo  시간  동안  연결이  확립되기를  기다린다.  그런  다음  tcp_current_mss()  함수를  호출하여  패킷  헤더  부분을  제외한  실제  데이터  영역의  크기를  계산한다.

이제  실제  데이터  전송에  필요한  정보를  설정하는데  "hello"라는  문자열  하나의  데이터만을  가지고  있으므로  iovlen  =  1,  iov->iov_base  =  "hello",  iov->iov_len  =  5로  설정되어  있을  것이다.  copied는  실제  전송된  데이터의  양을  나타내는  변수로  처음에는  0으로  설정한다.  그리고  현재까지  실행되는  동안  에러가  발생됐는지  소켓이  닫혔는지를  검사하여  이  경우  적절한  처리를  하고  전송을  종료한다.


  while  (--iovlen  >=  0)  {
  int  seglen  =  iov->iov_len;
  unsigned  char  __user  *from  =  iov->iov_base;

  iov++;

  while  (seglen  >  0)  {
    int  copy;

    skb  =  sk->sk_write_queue.prev;

    if  (!sk->sk_send_head  ||
      (copy  =  mss_now  -  skb->len)  <=  0)  {

new_segment:
      /*  Allocate  new  segment.  If  the  interface  is  SG,
      *  allocate  skb  fitting  to  single  page.
      */
      if  (!sk_stream_memory_free(sk))
        goto  wait_for_sndbuf;

      skb  =  sk_stream_alloc_pskb(sk,  select_size(sk,  tp),
        0,  sk->sk_allocation);
      if  (!skb)
        goto  wait_for_memory;

      /*
      *  Check  whether  we  can  use  HW  checksum.
      */
      if  (sk->sk_route_caps  &
        (NETIF_F_IP_CSUM  |  NETIF_F_NO_CSUM  |
        NETIF_F_HW_CSUM))
          skb->ip_summed  =  CHECKSUM_HW;

      skb_entail(sk,  tp,  skb);
      copy  =  mss_now;
  }

전송은  각각의  iov에  대하여  일어나므로  우리의  경우는  한번만  처리될  것이다.  현재  iov에  대하여  데이터(세그먼트)의  길이와  포인터를  각각  seglen,  from  변수에  저장한  후에  iov  포인터를  증가시킨다.  seglen=5이므로  while문  안으로  들어와서  skb  포인터를  소켓의  전송  큐  내에  있는  마지막  소켓  버퍼를  가리키도록  설정한다(아직은  아무런  소켓  버퍼도  들어있지  않다).  소켓이  최초에  생성되면  sk_send_head  필드가  NULL로  설정되므로  if문  안쪽의  new_segment  부분으로  들어가서  새로운  소켓  버퍼를  생성한다.

sk_stream_memory_free()  함수로  현재  소켓의  전송  버퍼(sndbuf)에  공간이  남아있는지  검사한  후  sk_stream_alloc_pskb()  함수를  이용하여  소켓  버퍼를  할당한다.  그리고  네트워크  장치에서  하드웨어  적으로  체크섬을  지원하는지를  검사하여  이  경우  하드웨어에서  처리할  수  있도록  skb->ip_summed  필드를  CHECKSUM_HW  로  표시한다.  이렇게  생성된  소켓  버퍼는  skb_entail()  함수를  이용하여  전송  일련번호를  설정한  후에  소켓  구조체의  전송  큐에  넣어진다.  


  /*  Where  to  copy  to?  */
  if  (skb_tailroom(skb)  >  0)  {
    if  (copy  >  skb_tailroom(skb))
      copy  =  skb_tailroom(skb);
    if  ((err  =  skb_add_data(skb,  from,  copy))  !=  0)
      goto  do_fault;
  }  else  {
    int  merge  =  0;
    int  i  =  skb_shinfo(skb)->nr_frags;
    struct  page  *page  =  TCP_PAGE(sk);
    int  off  =  TCP_OFF(sk);

    if  (skb_can_coalesce(skb,  i,  page,  off)  &&
      off  !=  PAGE_SIZE)  {
      merge  =  1;
    }  else  if  (i  ==  MAX_SKB_FRAGS  ||
      (!i  &&
        !(sk->sk_route_caps  &  NETIF_F_SG)))  {
    tcp_mark_push(tp,  skb);
      goto  new_segment;
    }  else  if  (page)  {
      off  =  (off  +  L1_CACHE_BYTES  -  1)  &
      ~(L1_CACHE_BYTES  -  1);
    if  (off  ==  PAGE_SIZE)  {
      put_page(page);
    TCP_PAGE(sk)  =  page  =  NULL;
        }
    }

    if  (!page)  {
      /*  Allocate  new  cache  page.  */
      if  (!(page  =  sk_stream_alloc_page(sk)))
      goto  wait_for_memory;
      off  =  0;
    }

    if  (copy  >  PAGE_SIZE  -  off)
      copy  =  PAGE_SIZE  -  off;

    err  =  skb_copy_to_page(sk,  from,  skb,  page,
        off,  copy);
    if  (err)  {
      if  (!TCP_PAGE(sk))  {
      TCP_PAGE(sk)  =  page;
      TCP_OFF(sk)  =  0;
      }
      goto  do_error;
    }

    /*  Update  the  skb.  */
    if  (merge)  {
      skb_shinfo(skb)->frags[i  -  1].size  +=
        copy;
    }  else  {
      skb_fill_page_desc(skb,  i,  page,  off,  copy);
      if  (TCP_PAGE(sk))  {
      get_page(page);
      }  else  if  (off  +  copy  <  PAGE_SIZE)  {
      get_page(page);
      TCP_PAGE(sk)  =  page;
      }
    }

    TCP_OFF(sk)  =  off  +  copy;
  }

다음으로  소켓  버퍼의  공간(tailroom)이  남아있다면  이  공간에  skb_add_data()  함수를  이용하여  데이터를  복사한다.  남은  공간이  없다면  skb_can_coalesce()  함수를  호출하여  소켓의  전송  메시지를  위한  페이지  내에  복사할  수  있는지  검사하고,  그렇지  않고  네트워크  장치가  Scatter-Gather  I/O를  지원하지  않거나  이미  MAX_SKB_FRAGS  만큼의  단편화(fragmentation)가  이뤄졌다면  new_segment  부분으로  돌아가서  새로운  소켓  버퍼를  생성한다.  만일  이미  페이지가  꽉  차  있다면  페이지를  해제하고  새로운  페이지를  할당받아  skb_copy_to_page()  함수를  이용하여  페이지에  데이터를  복사한다.  그리고  데이터가  복사된  정보를  소켓  버퍼의  데이터에  해당하는  skb_share_info  구조체에  기록한  후  소켓의  페이지와  오프셋  정보도  갱신한다.


    if  (!copied)
TCP_SKB_CB(skb)->flags  &=  ~TCPCB_FLAG_PSH;

    tp->write_seq  +=  copy;
    TCP_SKB_CB(skb)->end_seq  +=  copy;
    skb_shinfo(skb)->tso_segs  =  0;

    from  +=  copy;
    copied  +=  copy;
    if  ((seglen  -=  copy)  ==  0  &&  iovlen  ==  0)
        goto  out;

    if  (skb->len  !=  mss_now  ||  (flags  &  MSG_OOB))
        continue;

    if  (forced_push(tp))  {
        tcp_mark_push(tp,  skb);
        __tcp_push_pending_frames(sk,  tp,  mss_now,  TCP_NAGLE_PUSH);
    }  else  if  (skb  ==  sk->sk_send_head)
        tcp_push_one(sk,  mss_now);
    continue;
    ...
out:
    if  (copied)
        tcp_push(sk,  tp,  flags,  mss_now,  tp->nonagle);
    TCP_CHECK_TIMER(sk);
    release_sock(sk);
    return  copied;

copied  변수가  0이라면  TCP  헤더의  PSH  플래그를  지우고  전송  일련번호를  갱신한  뒤  from과  copied  변수도  복사된  만큼  증가시킨다.  첫  번째  if문의  조건을  만족하므로  out  부분으로  이동한  뒤  tcp_push()  함수를  이용하여  패킷을  전송한다.

tcp_push()  함수는  __tcp_push_pending_frames()  함수를  호출하고  이  함수는  다시  tcp_write_xmit()  함수를  호출한다.  tcp_write_xmit()  함수는  소켓  내의  전송될  소켓  버퍼(sk_send_head)에  대해  tcp_snd_test()를  호출하여  해당  소켓  버퍼를  전송할지  큐에  넣을지  결정한  후  tcp_transmit_skb()  함수를  호출한다.  이  함수는  소켓의  TCP  연산을  나타내는  tcp_func  구조체의  queue_xmit  필드가  가리키는  함수를  호출하는  데  이  함수는  IP  계층의  ip_queue_xmit()에  해당한다.

네트워크  계층  -  IP
ipqueue_xmit()  함수는  <net/ipv4/ip_output.c>에  정의되어  있다.  이  함수는  크게  두  부분으로  나눌  수  있는데  먼저  앞부분은  커널의  라우팅  테이블을  검색하여  패킷이  전송될  목적지의  주소를  알아내는  일이다.  먼저  해당  소켓으로  이미  다른  패킷을  보내서  목적지에  대한  캐시  데이터를  가지고  있다면  이  과정을  생략한다.  그리고  __sk_dst_check()  함수를  호출하여  만약  목적지에  대한  정보를  가지고  있지  않거나  더  이상  사용할  수  없는  데이터인  경우에는  새로  라우팅  테이블을  검색하도록  한다.  

검색에  필요한  정보는  flowi  구조체에  저장하며  전송할  인터페이스  정보,  출발지와  목적지의  네트워크  주소  및  포트  번호,  프로토콜과  TOS(Type  of  Service)  정보  등이  저장된다.  이렇게  생성한  정보를  가지고  ip_route_output_flow()  함수를  호출하면  검색  결과가  rtable  구조체에  저장되고  이를  소켓과  소켓  버퍼에  저장한다.

여기까지  왔다면  목적지에  대한  라우팅  정보를  가지고  있는  경우이다.  먼저  Strict  Source  Routing  옵션이  설정되어  있는  경우  목적지가  정해진  경로와  다르다면  에러로  처리한다.  그리고  나서  IP  헤더  정보를  설정한다.  IP  옵션이  주어진  경우에는  ip_options_build()  함수를  이용하여  옵션  정보를  생성한다.  그리고  ip_select_ident_more()  함수를  호출하여  fragment  ID를  설정한  뒤  ip_send_check()  함수에서  checksum  값을  계산한다.

마지막으로  소켓  버퍼의  우선순위를  설정한  후  NF_IP_LOCAL_OUT이라는  Netfilter  Hook으로  넘겨  패킷을  필터링할지를  검사한  다음에  NF_HOOK()  매크로의  마지막  인자로  주어진  dst_output()  함수를  호출한다.  dst_output()  함수는  소켓  버퍼의  목적지  정보를  가지는  dst  구조체의  output  필드가  가리키는  함수를  호출한다.  이것은  ip_route_output_flow()  함수를  처리하는  과정에서  ip_output()  함수로  설정된다.

ip_output()  함수는  우선  패킷의  전송이  요청됐음을  나타내는  통계  정보(IPSTATS_MIB_OUTREQUESTS)를  증가시킨다.  소켓  버퍼의  데이터의  길이를  검사하여  현재  목적지로  보낼  수  있는  최대  전송  크기(MTU:  Maximum  Transfer  Unit)보다  큰  경우에는  ip_fragment()  함수를  호출하여  패킷을  나눠서  보내고,  그렇지  않은  경우에는  ip_finish_output()  함수를  호출하여  그대로  패킷을  전송한다.  우리의  경우  데이터의  길이는  5이므로  ip_finish_output()  함수가  호출될  것이다.


packet_routed:
  if  (opt  &&  opt->is_strictroute  &&  rt->rt_dst  !=  rt->rt_gateway)
      goto  no_route;

  /*  OK,  we  know  where  to  send  it,  allocate  and  build  IP  header.  */
  iph  =  (struct  iphdr  *)  skb_push(skb,  sizeof(struct  iphdr)  +  (opt  ?  opt->optlen  :  0));
  *((__u16  *)iph)  =  htons((4  <<  12)  |  (5  <<  8)  |  (inet->tos  &  0xff));
  iph->tot_len  =  htons(skb->len);
  if  (ip_dont_fragment(sk,  &rt->u.dst)  &&  !ipfragok)
    iph->frag_off  =  htons(IP_DF);
  else
    iph->frag_off  =  0;
  iph->ttl  =  ip_select_ttl(inet,  &rt->u.dst);
  iph->protocol  =  sk->sk_protocol;
  iph->saddr  =  rt->rt_src;
  iph->daddr  =  rt->rt_dst;
  skb->nh.iph  =  iph;
  /*  Transport  layer  set  skb->h.foo  itself.  */

  if  (opt  &&  opt->optlen)  {
    iph->ihl  +=  opt->optlen  >>  2;
    ip_options_build(skb,  opt,  inet->daddr,  rt,  0);
  }

  ip_select_ident_more(iph,  &rt->u.dst,  sk,  skb_shinfo(skb)->tso_segs);

  /*  Add  an  IP  checksum.  */
  ip_send_check(iph);

  skb->priority  =  sk->sk_priority;

  return  NF_HOOK(PF_INET,  NF_IP_LOCAL_OUT,  skb,  NULL,  rt->u.dst.dev,
      dst_output);

no_route:
  IP_INC_STATS(IPSTATS_MIB_OUTNOROUTES);
  kfree_skb(skb);
  return  -EHOSTUNREACH;
}

ip_finish_output()  함수는  소켓  버퍼의  장치  정보와  프로토콜  정보를  설정한  뒤  NF_IP_POST_ROUTING  Netfilter  Hook을  통해  ip_finish_output2()  함수를  호출한다.  ip_finish_output2()  함수는  <net/ipv4/ip_output.c>에  정의되어  있다.

이  함수는  먼저  현재  소켓  버퍼  내에  데이터  링크  계층의  헤더  정보(dev->hard_header)가  들어갈  만한  공간이  있는지  검사하여  없는  경우  skb_realloc_headroom()  함수를  호출하여  공간을  확보한다.  그리고  넷필터  디버깅  옵션이  설정되어  있는  경우  nf_debug_ip_finish_output2()  함수를  호출하여  필요한  메시지를  출력한다.  

그리고  목적지에  대한  하드웨어  헤더  캐시  정보를  가지고  있다면  캐시에  포함된  헤더  정보를  소켓  버퍼에  저장하고,  캐시의  hh_output  필드가  가리키는  함수를  호출한다.  캐시  정보를  가지고  있지  않다면  직접  다음  번  전송될  목적지(neighbour)에  대한  output  필드가  가리키는  함수를  호출한다.  output  필드는  neigh_resolve_output()  함수를  가리킨다.  이  함수는  내부적으로  neigh_opt  구조체의  queue_xmit  필드가  가리키는  함수를  호출하는데  hh_output  필드가  가리키는  것과  동일하게  dev_queue_xmit()  함수를  가리킨다.  이제  dev_queue_xmit()  함수를  따라  데이터  링크  계층으로  내려가  보자.

데이터  링크  계층  -  pci-skeleton
dev_queue_xmit()  함수는  IP  계층에서  처리된  소켓  버퍼를  실제  네트워크  장치에게로  넘겨  전송하는  역할을  하며  <net/core/dev.c>에  정의되어  있다.

우선  소켓  버퍼가  소켓의  데이터  전송용  페이지(sk_sndmsg_page)  내에  fragment로  나눠져  있다.  하지만  전송할  네트워크  장치에서  fragment  혹은  SG(Scatter-Gather)  I/O를  지원하지  않거나,  하나  이상의  fragment가  장치에서  DMA로  접근할  수  없는  영역에  있다면  __skb_linearize()  함수를  이용하여  가능한  영역  내의  하나의  데이터로  합친다.  그리고  checksum이  아직  계산되지  않았다면  여기서  skb_checksum_help()  함수를  이용하여  계산하고  local_bh_disable()  매크로를  이용하여  현재  CPU에  대해  softirq를  금지시킨다.


  if  (q->enqueue)  {
    spin_lock(&dev->queue_lock);

    rc  =  q->enqueue(skb,  q);

    qdisc_run(dev);

    spin_unlock(&dev->queue_lock);
    rc  =  rc  ==  NET_XMIT_BYPASS  ?  NET_XMIT_SUCCESS  :  rc;
    goto  out;
  }

  if  (dev->flags  &  IFF_UP)  {
    int  cpu  =  smp_processor_id();  /*  ok  because  BHs  are  off  */

    if  (dev->xmit_lock_owner  !=  cpu)  {

      HARD_TX_LOCK(dev,  cpu);

      if  (!netif_queue_stopped(dev))  {
        if  (netdev_nit)
          dev_queue_xmit_nit(skb,  dev);

        rc  =  0;
        if  (!dev->hard_start_xmit(skb,  dev))  {
          HARD_TX_UNLOCK(dev);
          goto  out;
        }
      }
      HARD_TX_UNLOCK(dev);
      if  (net_ratelimit())
        printk(KERN_CRIT  "Virtual  device  %s  asks  to  "
          "queue  packet!\n",  dev->name);
    }  else  {
      if  (net_ratelimit())
        printk(KERN_CRIT  "Dead  loop  on  virtual  device  "
          "%s,  fix  it  urgently!\n",  dev->name);
    }
  }

  rc  =  -ENETDOWN;
  local_bh_enable();

out_kfree_skb:
  kfree_skb(skb);
  return  rc;
out:
  local_bh_enable();
  return  rc;
}

이제  실제로  각  장치에  해당하는  전송  함수를  불러  패킷을  전송할  차례이다.  먼저  해당  장치에서  전송될  데이터를  위한  큐를  지원한다면(q->enqueue),  이  큐에  소켓  버퍼를  집어넣고  qdisc_run()  함수를  이용하여  전송한다.  qdisc_run()  함수는  다시  qdisc_restart()  함수를  호출하여  현재  네트워크  장치가  패킷을  전송할  수  있는지  검사하한다(netif_queue_stopped(dev)).  여기서  전송할  수  있다면  dev  구조체의  hard_start_xmit  필드가  가리키는  함수를  호출하여  네트워크  장치에게  넘기고,  그렇지  않다면  다시  큐에  넣고  netif_schedule()  함수를  이용하여  NET_TX_SOFTIRQ를  발생시켜  이후에  전송되도록  한다.

해당  장치에서  큐를  지원하지  않는다면  직접  전송하는데  먼저  현재  장치에  대한  장치에  대한  락을  가지고  있는  CPU를  검사하여  만약  이미  락을  가지고  있는  경우라면  무언가  잘못된  경우이므로  에러로  처리한다.  그렇지  않다면  netif_queue_stopped()  함수를  이용하여  장치가  패킷을  전송할  수  있는지  검사한  후  역시  hard_start_xmit  필드가  가리키는  함수를  호출한다.  이  함수는  각  장치의  드라이버  내에  위치하고  있으며  이에  대한  일반적인  형태로  <drivers/net/isa-skeleton.c>  파일  내의  net_send_packet()  함수  혹은  <drivers/net/pci-skeleton.c>  파일  내의  netdrv_start_xmit()  함수를  참조하기  바란다.

패킷의  수신
이제  네트워크  장치를  통해  받은  패킷이  처리되는  과정에  대해  살펴보자.

데이터  링크  계층  -  pci-skeleton
네트워크  장치가  패킷을  수신하면  인터럽트가  발생한다.  이  인터럽트에  대한  처리는  각  장치에  따라  다르므로  여기서는  <drivers/net/pci-skeleton.c>  파일에서  구현한  PCI  버스를  사용하는  일반적인  네트워크  장치에  대한  부분을  살펴볼  것이다.  먼저  이  장치의  open()  함수에서  다음과  같이  인터럽트를  등록한다.


static  int  netdrv_open  (struct  net_device  *dev)
{
    ...

    retval  =  request_irq  (dev->irq,  netdrv_interrupt,  SA_SHIRQ,  dev->name,  dev);
    if  (retval)  {
        DPRINTK  ("EXIT,  returning  %d\n",  retval);
        return  retval;
    }

    ...
}

장치의  irq  번호에  해당하는  인터럽트에  대해  netdrv_interrupt()  함수를  등록한다.  이  함수는  장치의  상태  레지스터의  값을  읽어  전송(TX)과  수신(RX)에  해당하는  인터럽트  처리  함수를  호출하는데  여기서는  netdrv_rx_interrupt()  함수가  호출된다.  이  함수에서는  dev_alloc_skb()  함수를  이용하여  소켓  버퍼를  생성하고,  eth_copy_and_sum()  함수를  이용하여  데이터를  복사한  뒤,  eth_type_trans()  함수를  호출하여  하드웨어  헤더  정보를  설정하고  이더넷  프로토콜  정보를  리턴한다.  그리고  netif_rx()  함수를  호출하여  소켓  버퍼를  현재  CPU의  softnet_data  구조체의  input_pkt_queue에  넣고,  dev  구조체의  poll_list  정보를  softnet_data  구조체의  poll_list에  추가한  후  NET_RX_SOFTIRQ를  발생시킨다.  그리고  패킷  수신에  대한  통계  정보를  갱신한  후에  인터럽트를  처리를  마친다.  나머지  부분은  softirq로  처리하며  이에  대한  처리는  <net/core/dev.c>에  정의된  net_rx_action()  함수가  맡고  있다.

이  함수는  현재  CPU의  softnet_data  구조체의  poll_list에  대하여  처리를  한다.  그  자료  구조에  접근하는  동안에는  인터럽트로  인해  새로운  패킷이  추가되지  않도록  인터럽트를  금지시켜야  한다.  장치가  처리할  수  있는  양을  넘었거나  너무  많은  시간이  흐른  경우에는  다음  번  softirq  시점에서  처리하도록  softnet_break  부분으로  이동하여  리턴하고,  그렇지  않다면  dev  구조체의  poll  필드가  가리키는  함수를  호출한다.  이  함수는  process_backlog()에  해당하며  softnet_data  구조체의  input_pkt_queue  구조체  내의  소켓  버퍼  정보를  하나씩  꺼내서  netif_receive_skb()  함수를  호출한다.  이  함수는  하드웨어  헤더  정보를  읽어  적절한  처리를  한  후  패킷의  프로토콜에  해당하는  packet_type  구조체의  처리  함수를  호출한다.  이  경우  ip_packet_type  구조체의  ip_rcv()  함수가  호출될  것이다.

네트워크  계층  -  IP
이  함수는  먼저  자신에게  보내진  패킷이  아니라면  버린다.  그리고  통계  정보를  갱신한  후  소켓  버퍼의  데이터가  공유되고  있는지  검사하여  그런  경우  소켓  버퍼  구조체  자체를  복사(clone)한다.  그리고  pskb_may_pull()  함수를  호출하여  받은  패킷의  데이터  길이가  IP  헤더  정보를  포함하는  길이인지를  검사한  후  이를  IP  헤더로  인식한다.  그리고  RFC1122의  패킷  거부(discard)  규정에  따라  다음  4가지  사항을  점검한다.

①  패킷의  길이가  IP  헤더  정보의  길이보다  작지는  않은가?  ②IP  버전의  4인가?  ③checksum이  올바른가?  ④패킷의  길이  정보가  올바른가?  여기서  IP  헤더에  포함된  헤더  길이  정보(ihl  필드에  해당)는  4의  배수의  형태로  기록되어  있으므로  실제  길이와  비교하기  위해서는  4를  곱하는  형태가  되어야  한다(ihl  *  4  혹은  ihl  <<  2).  혹은  IP  헤더의  최소  길이는  20바이트이므로  이를  검사하기  위해서는  ihl  필드가  5보다  작은지  검사할  수도  있다.  이  단계를  거친  올바른  패킷이라면  NF_IP_PRE_ROUTING  Netfilter  Hook을  거쳐  ip_rcv_finish()  함수를  호출한다.

ip_rcv_finish()  함수는  먼저  ip_route_input()  함수를  호출하여  리눅스  상에서  패킷을  처리하기  위한  목적지  정보를  설정한다.  최종  목적지가  자기  자신이라면  소켓  버퍼의  dst  구조체의  input  필드가  ip_local_deliver()  함수를  가리키도록  설정된다.  그렇지  않고  자신을  거쳐  다른  호스트에게  보내지는  패킷의  경우에는  ip_forward()  함수로  설정된다.  그리고  IP  헤더  내에  옵션  정보가  포함되어  있다면(헤더의  길이가  20  바이트보다  커지므로  ihl  필드가  5보다  크다)  ip_options_complie()  함수를  호출하여  ip_options  구조체의  형태로  만든다.  그리고  dst_input()  함수를  호출하여  dst  구조체의  input  필드가  가리키는  ip_local_deliver()  함수를  호출한다.

ip_local_deliver()  함수는  패킷이  fragment라면  ip_defrag()  함수를  호출하여  ipq  구조체에  저장하고,  그렇지  않다면  NF_IP_LOCAL_IN이라는  Netfilter  Hook을  통해  ip_local_deliver_finish()  함수를  호출한다.  이  함수는  먼저  IP  헤더  길이만큼  데이터를  이동시켜  TCP  헤더  정보로  설정한  뒤  해당  프로토콜에  해당하는  정보를  찾아  상위  프로토콜로  넘겨주는  일을  한다.  이  경우  tcp_protocol  구조체의  tcp_v4_rcv()  함수가  호출된다.

전송  계층  -  TCP
tcp_v4_rcv()  함수는  주어진  TCP  헤더  정보에  따라  적절한  처리를  한  뒤  소켓  버퍼의  출발지와  목적지의  네트워크  주소  및  포트  번호를  통해  __tcp_v4_lookup()  함수를  호출하여  그에  해당하는  소켓  정보를  찾아낸다.  그리고  소켓에  필터가  존재하는  경우  sk_filter()  함수를  통해  필터링을  하고  tcp_v4_do_rcv()  함수를  호출한다.  이  함수는  <net/ipv4/tcp_ipv4.c>에  정의되어  있다.

이  함수는  현재  소켓의  상태에  따라  각각  다른  처리  함수를  호출한다.  먼저  일반적으로  소켓이  연결된  상태라면  (TCP_ESTABLISHED)  tcp_rcv_established()  함수를  호출하고,  그렇지  않고  소켓을  기다리는  중이라면  (TCP_LISTEN)  tcp_v4_hnd_req()  함수를  호출하여  연결에  대한  요청을  처리한다.  그  외의  상태에  대해서는  connect()  시스템  콜에  대한  부분에서  간략히  살펴본  대로  tcp_rcv_state_process()  함수가  호출된다.  tcp_rcv_established()  함수는  ACK에  대한  처리와  타임스탬프,  수신  일련번호  및  윈도우에  대한  처리를  한  후에  사용자  공간으로  데이터를  복사해  준다.  

이  때  softirq를  처리하는  프로세스(current)가  소켓을  기다리는  프로세스라면  현재  프로세스의  상태를  TASK_RUNNING으로  만들고  tcp_copy_to_iovec()  함수를  통해  직접  데이터를  복사한다.  그렇지  않다면  소켓  버퍼를  소켓  구조체의  sk_receive_queue의  맨  마지막에  넣고  소켓  버퍼의  소유자를  해당  소켓으로  설정한  뒤  소켓  구조체의  sk_data_ready  필드가  가리키는  함수를  호출한다.  이  함수는  sock_def_readable()  함수로  설정되어  있으며,  sk  구조체의  sk_sleep  필드가  가리키는  wait_queue에서  잠들어  있는  프로세스들을  깨운다.

응용  계층
응용  프로그램에서  read()  시스템  콜을  호출하면  write()의  경우와  마찬가지로  sys_read()→vfs_read()→do_sync_read()→sock_aio_read()  함수를  거쳐  __sock_recvmsg()  함수가  호출되며,  <net/socket.c>에  정의되어  있다.

이  함수는  소켓  I/O  연산을  위한  sock_iocb  구조체를  초기화한  뒤,  security_socket_recvmsg()  함수를  호출하여  보안  사항을  점검하고  실제  루틴인  BSD  소켓  구조체의  ops  구조체의  recvmsg  필드가  가리키는  함수를  호출한다.  inet_stream_ops  구조체의  recvmsg  필드는  sock_common_recvmsg()  함수를  가리키고  있으며,  이  함수는  다시  INET  소켓의  recvmsg  필드가  가리키는  함수를  호출한다.  TCP에서  이  함수는  tcp_sendmsg()에  해당한다.  
이  함수는  루프를  돌며  sk_receive_queue  내의  소켓  버퍼를  검사하여  원하는  offset  에  해당하는  소켓  버퍼의  데이터를  찾아  복사한다.  이  과정에서  프로세스가  시그널을  받는다면  sock_rcvtimeo()  함수에서  계산된  timeo  값에  따라  -ERESTARTSYS  혹은  -EINTR  에러와  함께  리턴된다.

sk_receive_queue에서  해당하는  소켓  버퍼를  찾지  못하면  sk_wait_data()  함수를  호출하여  timeo  시간만큼  기다린다.  이  때  프로세스는  INET  소켓  구조체의  sk_sleep  필드가  가리키는  wait_queue에서  잠든다.  이  후에  패킷을  받으면  tcp_rcv_established()에서  이  wait_queue  내의  프로세스들을  깨우게  될  것이다.

계층별  패킷  흐름  정리
지금껏  네트워크  패킷이  송/수신되는  과정을  패킷의  전달  과정을  따라가며  살펴보았다.  지금부터  이를  간단히  블럭도로  정리하여  각  계층에서  패킷의  송/수신을  처리하는  것을  단계  별로  살펴보기로  한다.  <그림  3>에서  왼쪽이  수신  과정,  오른쪽이  송신  과정을  보여준다.

데이터  링크  계층
<그림  3>은  데이터  링크  계층에서  패킷이  송/수신  되는  과정을  보여준다.  패킷이  수신되면  인터럽트  처리  루틴에  의해  소켓  버퍼를  생성하여  데이터를  복사하고  하드웨어  헤더  정보를  설정한  뒤  현재  CPU의  수신  큐에  넣고  softirq에게로  처리를  넘긴다.  패킷을  송신할  때는  일단  네트워크  장치마다  할당된  큐에  소켓  버퍼를  넣고  현재  송신이  가능한지를  검사하여  디바이스  드라이버의  hard_start_xmit  루틴을  호출하여  직접  송신하거나  softirq를  발생시켜  이후에  송신하도록  한다.  앞에서  디바이스  드라이버  계층의  함수는  <drivers/net/pci-skeleton.c>  내의  처리  함수들에  해당한다.





네트워크  계층  -  IP
<그림  4>는  네트워크  계층의  패킷  전송도이다.  커널의  컴파일  과정에서  네트워크  필터링(network  filtering)  기능이  포함되면  패킷의  전송  과정에서  다음과  같은  5가지의  Netfilter  Hook을  거치는  데  각각의  역할은  다음과  같다.  


◆  NF_IP_PRE_ROUTING  :  네트워크  장치로부터  수신된  모든  패킷을  처리한다.  실제로  패킷에  대한  처리가  이루어지기  전에  필터링이  가능하게  되므로  DOS  공격에  대한  처리나  목적지  네트워크  주소  변환(DNAT)의  처리,  통계  정보  기록  등을  하기에  알맞다.
◆  NF_IP_LOCAL_IN  :  로컬  머신에게  전송된  패킷만을  처리한다.
◆  NF_IP_FORWARD  :  로컬  머신을  통해  다른  머신에게로  forwarding되는  패킷만을  처리한다.
◆  NF_IP_LOCAL_OUT  :  로컬  머신에서  송신하는  패킷만을  처리한다.
◆  NF_IP_POST_ROUTING  :  네트워크  장치를  통해  송신하는  모든  패킷을  처리한다(forwarding  패킷  포함).  출발지  네트워크  주소  변환(SNAT)이나  masquerading의  처리,  통계  정보  기록  등을  하기에  알맞다.





전송  계층  -  TCP
<그림  5>는  전송  계층의  패킷  처리  과정을  보여준다.  TCP  계층에서  패킷을  수신하면  현재  소켓의  상태에  따라  각기  다른  함수가  호출된다.  소켓이  연결된  상태의  처리  함수인  tcp_rcv_established()는  소켓  버퍼의  데이터를  사용자  공간의  버퍼에  복사하며  이를  기다리며  잠든  프로세스가  있다면  깨운다.  송신  과정에서는  소켓  버퍼를  생성하여  데이터를  복사하고  모든  계층의  헤더  정보가  들어갈  만한  공간을  확보해  둔다.





커널  해커가  많이  등장하길
이번  글에서는  리눅스의  네트워크  서브  시스템에  대해  간략하게  살펴보았다.  물론  이  밖에도  더  많은  부분이  있지만  필자의  부족한  실력과  한정된  지면으로  인해  더  소개하지  못한  것이  아쉽기만  할  따름이다.

항상  시작할  때는  마음만  앞서서  많은  것을  소개하려고  하다가  뒤로  갈수록  필자의  한계를  깨닫고  용두사미의  형태로  진행되는  것  같아  부끄럽고  독자들에게  죄송스러운  마음이  든다.  앞으로  국내에서도  훌륭한  커널  해커가  많이  등장하여  리눅스  진영에서  활약을  해  주길  기대하며  3회에  걸친  리눅스  커널  2.6에  대한  연재를  마치고자  한다.@
















[본문스크랩]  [리눅스]  ③  vim  편집기  활용법  |  linux  
2006/04/02  14:19  



http://blog.naver.com/wono77/140023052074



  블로그  >  만두양  관심거리  ^-^
  http://blog.naver.com/imandu/120023264873  


사용자의  입장에서  윈도우와  가장  두드러지게  눈에  띄는  리눅스,  그리고  다른  유닉스  운영체제의  차이점은  무엇일까?  여러  차이점,  특히  관습이나  문화적인  차이점도  많이  있겠지만  vi  에디터의  독특함도  중요하게  작용하고  있다고  본다.

최근  들어서는  유닉스를  처음  배우면서  에디터로  vi를  반드시  배워야  하는  상황은  많이  줄었지만  여전히  vi는  많이  쓰이고  있으며  그  편리함과  독특한  매력은  많은  사용자를  확보하고  있다.  아직까지  vi를  제대로  쓰지  않고  리눅스를  사용하고  있다면  이번  기회에  조금  귀찮더라도  반드시  vi  기본  명령어는  습득해  보도록  하고,  또  추가로  많이  쓰이는  vi  관련  설정을  살펴보기로  하자.

vi를  처음  접하는  사람에게  가장  까다롭게  느껴지는  것은  vi가  소위  명령어  모드(command  mode)와  입력  모드(input  mode)가  나뉘어져  있다는  특성  때문이다.  vi를  처음  실행시키고  타이핑을  해보면  아무런  글자도  입력되지  않고  심지어는  비프음까지  동반하게  되는데  많은  사용자들이  이런  vi의  편집기답지  않은  황당한(?)  모습에  금방  거부감을  갖게  되는  경우가  많다.  하지만  일단  vi의  명렁어  모드와  입력  모드의  차이점을  인식하고  몇  가지  커서  이동  명령어만  학습하고  나면  vi의  매력을  금방  느낄  수  있으니  겉모습만을  보고  오해해서  vi를  버리지  말도록  하자.

vim은  정확히  말하자면  여러  vi  클론들  중의  하나이다.  vim의  이름은  vi  improved에서  따왔다고  하는데  vim은  기본적인  vi의  기능에  덧붙여서  여러  개의  파일을  동시에  편집한다든가  프로그램  소스코드에  색깔을  덧붙여  가독성을  높여주는  syntax  highlighting과  같은  기능을  추가로  지원한다.  

pico,  nano
우선,  vi를  배우는  동안  간단히  쓰기  좋은  에디터로는  pico나  pico의  자유  소프트웨어  클론인  nano를  권장한다(젠투  리눅스는  시스템  설치  과정에서  nano를  사용한다).  pico나  nano는  어느  배포판이나  기본으로  설치되어  있는  경우가  많으며  명령어  안내가  간단하게  화면  아래쪽에  출력되기  때문에  불편하지만  쉽게  쓸  수  있다.  오래  전  PC통신  시절의  경험이  있는  사용자라면  이미  pico를  많이  사용해  보았을  수도  있다.  pico의  사용  방법은  무척  간단하기  때문에  여기서는  생략하도록  한다.  

명령어  모드와  입력  모드
앞에서  잠깐  얘기했듯이  vi는  명령어  모드와  입력  모드가  나뉘어서  동작한다.  예를  들어  명령어  모드에서는  h  키를  누르면  왼쪽  화살표  키와  같이  커서가  왼쪽으로  한  칸  이동하지만  입력  모드에서는  화면에  h  글자가  입력된다.

vi는  실행되었을  때  기본으로  명령어  모드가  작동된다.  따라서  vi를  실행한  뒤  아무리  키를  눌러봐야  글자가  타이핑되지  않는다.  입력  모드로  들어가기  위해  i  키(insert,  삽입)를  눌러  보자.  i  키를  누른  이후로는  타이핑이  될  것이다.  참고로  a  키를  누르면  커서  다음  글자부터  입력이  된다.  추가로  대문자  I와  A의  차이점도  직접  확인해  보자.

서너  줄  정도  간단한  글을  입력한  다음  이제  다시  명령어  모드로  돌아가기  위해  esc  키를  눌러보자.  esc  키는  vi에서  가장  중요한  키  중의  하나이며  esc를  누르면  vi는  항상  명령어  모드로  돌아간다.  여기서  다시  i나  a  키를  누르면  삽입  모드로  들어간다(한  줄을  비우고  삽입  모드로  들어가는  o와  O도  시험해  보자).  이제  esc  키를  눌러서  명령어  모드로  돌아간  뒤  커서를  상하좌우로  이동해보자.

요즘은  상황이  많이  달라졌지만,  vi에서는  화면  상의  커서를  상하좌우로  이동하기  위해  키보드  위의  상하좌우  화살표  키를  원칙적으로  쓰지  않는다.  vim에서는  화살표  키를  써도  커서  이동이  가능하지만  일단은  vi에서는  화살표  키를  쓰지  않는  습관을  들여  보자.  vi에서는  커서  이동키로  h(좌),  j(하),  k(상),  l(우)을  사용한다.  한  페이지  위로  올라갈  때는  pgup  키  대신  ,  한  페이지  아래로  내려갈  때에는  pgdn  키  대신  를  사용한다.  <표  1>를  참조해서  커서  이동키를  외우도록  하자.











<표  1>  vi의  커서  이동  키



   
















명령어



내용




왼쪽



h




오른쪽



l




위쪽



k




아래쪽



j




한  페이지  위로



<ctrl+v>




한  페이지  아래로



<ctrl+f>












vi에서는  왜  이렇게  낯선  커서  키  배열을  사용할까?  그  이유  중  하나는  vi가  커서  키로  사용하고  있는  h,  j,  k,  l  키가  화살표  키보다  사용하기  훨씬  편리하기  때문이다.  다만  vi의  키  배열은  아무래도  처음  익숙해지고  외우기까지가  불편하다.  하지만  vi의  명령어  키는  모두  기본  자판만을  사용하기  때문에  커서를  움직이거나  페이지를  스크롤  하기  위해  오른손을  들 [본문링크] [리눅스 커널 2.6의 세계] ③ 네트워크 서브 시스템 [1] 
코멘트(이글의 트랙백 주소:/cafe/tb_receive.php?no=532
작성자
비밀번호

 

SSISOCommunity

[이전]

Copyright byCopyright ⓒ2005, SSISO Community All Rights Reserved.